const defSepCodes = [38]; // &
const defEqCodes = [61]; // =

const unhexTable = new Int8Array([
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0 - 15
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 16 - 31
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 32 - 47
    +0, +1, +2, +3, +4, +5, +6, +7, +8, +9, -1, -1, -1, -1, -1, -1, // 48 - 63
    -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 64 - 79
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 80 - 95
    -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 96 - 111
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 112 - 127
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 128 ...
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // ... 255
]);

const isHexTable = new Int8Array([
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 32 - 47
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
    0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64 - 79
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 95
    0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96 - 111
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 112 - 127
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 ...
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ... 256
]);

const noEscape = new Int8Array([
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
    0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, // 32 - 47
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 80 - 95
    0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 112 - 127
]);

const hexTable = new Array(256);
for (let i = 0; i < 256; ++i)
hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();

function parse(qs, sep, eq, options) {
    const obj = Object.create(null);

    if (typeof qs !== 'string' || qs.length === 0) {
        return obj;
    }

    const sepCodes = (!sep ? defSepCodes : charCodes(String(sep)));
    const eqCodes = (!eq ? defEqCodes : charCodes(String(eq)));
    const sepLen = sepCodes.length;
    const eqLen = eqCodes.length;

    let pairs = 1000;
    if (options && typeof options.maxKeys === 'number') {
        pairs = (options.maxKeys > 0 ? options.maxKeys : -1);
    }

    let decode = qsUnescape;
    if (options && typeof options.decodeURIComponent === 'function') {
        decode = options.decodeURIComponent;
    }
    const customDecode = (decode !== qsUnescape);

    let lastPos = 0;
    let sepIdx = 0;
    let eqIdx = 0;
    let key = '';
    let value = '';
    let keyEncoded = customDecode;
    let valEncoded = customDecode;
    const plusChar = (customDecode ? '%20' : ' ');
    let encodeCheck = 0;
    for (let i = 0; i < qs.length; ++i) {
        const code = qs.charCodeAt(i);

        if (code === sepCodes[sepIdx]) {
            if (++sepIdx === sepLen) {
                const end = i - sepIdx + 1;
                if (eqIdx < eqLen) {
                    if (lastPos < end) {
                        key += qs.slice(lastPos, end);
                    } else if (key.length === 0) {
                        if (--pairs === 0)
                        return obj;
                        lastPos = i + 1;
                        sepIdx = eqIdx = 0;
                        continue;
                    }
                } else if (lastPos < end) {
                    value += qs.slice(lastPos, end);
                }

                addKeyVal(obj, key, value, keyEncoded, valEncoded, decode);

                if (--pairs === 0)
                return obj;
                keyEncoded = valEncoded = customDecode;
                key = value = '';
                encodeCheck = 0;
                lastPos = i + 1;
                sepIdx = eqIdx = 0;
            }
        } else {
            sepIdx = 0;
            if (eqIdx < eqLen) {
                if (code === eqCodes[eqIdx]) {
                    if (++eqIdx === eqLen) {
                        // Key/value separator match!
                        const end = i - eqIdx + 1;
                        if (lastPos < end)
                        key += qs.slice(lastPos, end);
                        encodeCheck = 0;
                        lastPos = i + 1;
                    }
                    continue;
                } else {
                    eqIdx = 0;
                    if (!keyEncoded) {
                        if (code === 37 /* % */
                        ) {
                            encodeCheck = 1;
                            continue;
                        } else if (encodeCheck > 0) {
                            if (isHexTable[code] === 1) {
                                if (++encodeCheck === 3)
                                keyEncoded = true;
                                continue;
                            } else {
                                encodeCheck = 0;
                            }
                        }
                    }
                }
                if (code === 43 /* + */
                ) {
                    if (lastPos < i)
                    key += qs.slice(lastPos, i);
                    key += plusChar;
                    lastPos = i + 1;
                    continue;
                }
            }
            if (code === 43 /* + */
            ) {
                if (lastPos < i)
                value += qs.slice(lastPos, i);
                value += plusChar;
                lastPos = i + 1;
            } else if (!valEncoded) {
                if (code === 37 /* % */
                ) {
                    encodeCheck = 1;
                } else if (encodeCheck > 0) {
                    if (isHexTable[code] === 1) {
                        if (++encodeCheck === 3)
                        valEncoded = true;
                    } else {
                        encodeCheck = 0;
                    }
                }
            }
        }
    }

    if (lastPos < qs.length) {
        if (eqIdx < eqLen)
        key += qs.slice(lastPos);
        else if (sepIdx < sepLen)
        value += qs.slice(lastPos);
    } else if (eqIdx === 0 && key.length === 0) {
        return obj;
    }

    addKeyVal(obj, key, value, keyEncoded, valEncoded, decode);

    return obj;
}

function stringify(obj, sep, eq, options) {
    sep = sep || '&';
    eq = eq || '=';

    let encode = qsEscape;
    if (options && typeof options.encodeURIComponent === 'function') {
        encode = options.encodeURIComponent;
    }
    const convert =
        (encode === qsEscape ? encodeStringified : encodeStringifiedCustom);

    if (obj !== null && typeof obj === 'object') {
        const keys = Object.keys(obj);
        const len = keys.length;
        let fields = '';
        for (let i = 0; i < len; ++i) {
            const k = keys[i];
            const v = obj[k];
            let ks = convert(k, encode);
            ks += eq;

            if (Array.isArray(v)) {
                const vlen = v.length;
                if (vlen === 0) continue;
                if (fields)
                fields += sep;
                for (let j = 0; j < vlen; ++j) {
                    if (j)
                    fields += sep;
                    fields += ks;
                    fields += convert(v[j], encode);
                }
            } else {
                if (fields)
                fields += sep;
                fields += ks;
                fields += convert(v, encode);
            }
        }
        return fields;
    }
    return '';
}

function qsEscape(str) {
    if (typeof str !== 'string') {
        if (typeof str === 'object')
        str = String(str);
        else
        str += '';
    }

    return encodeStr(str, noEscape, hexTable);
}

function qsUnescape(s, decodeSpaces) {
    try {
        return decodeURIComponent(s);
    } catch(e) {
        return unescapeBuffer(s, decodeSpaces).toString();
    }
}

function unescapeBuffer(s, decodeSpaces) {
    const out = []; //Buffer.allocUnsafe(s.length);//暂时用空代替
    let index = 0;
    let outIndex = 0;
    let currentChar;
    let nextChar;
    let hexHigh;
    let hexLow;
    const maxLength = s.length - 2;

    let hasHex = false;
    while (index < s.length) {
        currentChar = s.charCodeAt(index);
        if (currentChar === 43 /* '+' */
        && decodeSpaces) {
            out[outIndex++] = 32; // ' '
            index++;
            continue;
        }
        if (currentChar === 37 /* '%' */
        && index < maxLength) {
            currentChar = s.charCodeAt(++index);
            hexHigh = unhexTable[currentChar];
            if (!(hexHigh >= 0)) {
                out[outIndex++] = 37; // '%'
                continue;
            } else {
                nextChar = s.charCodeAt(++index);
                hexLow = unhexTable[nextChar];
                if (!(hexLow >= 0)) {
                    out[outIndex++] = 37; // '%'
                    index--;
                } else {
                    hasHex = true;
                    currentChar = hexHigh * 16 + hexLow;
                }
            }
        }
        out[outIndex++] = currentChar;
        index++;
    }
    return hasHex ? out.slice(0, outIndex) : out;
}

function addKeyVal(obj, key, value, keyEncoded, valEncoded, decode) {
    if (key.length > 0 && keyEncoded)
    key = decodeStr(key, decode);
    if (value.length > 0 && valEncoded)
    value = decodeStr(value, decode);

    if (obj[key] === undefined) {
        obj[key] = value;
    } else {
        const curValue = obj[key];
        if (curValue.pop)
        curValue[curValue.length] = value;
        else
        obj[key] = [curValue, value];
    }
}

function encodeStr(str, noEscapeTable, hexTable) {
    const len = str.length;
    if (len === 0)
    return '';

    let out = '';
    let lastPos = 0;
    let i = 0;

    outer:
    for (; i < len; i++) {
        let c = str.charCodeAt(i);

        // ASCII
        while (c < 0x80) {
            if (noEscapeTable[c] !== 1) {
                if (lastPos < i)
                out += str.slice(lastPos, i);
                lastPos = i + 1;
                out += hexTable[c];
            }

            if (++i === len)
            break outer;

            c = str.charCodeAt(i);
        }

        if (lastPos < i)
        out += str.slice(lastPos, i);

        // Multi-byte characters ...
        if (c < 0x800) {
            lastPos = i + 1;
            out += hexTable[0xC0 | (c >> 6)] +
            hexTable[0x80 | (c & 0x3F)];
            continue;
        }
        if (c < 0xD800 || c >= 0xE000) {
            lastPos = i + 1;
            out += hexTable[0xE0 | (c >> 12)] +
            hexTable[0x80 | ((c >> 6) & 0x3F)] +
            hexTable[0x80 | (c & 0x3F)];
            continue;
        }
        // Surrogate pair
        ++i;

        if (i >= len)
        return '';

        const c2 = str.charCodeAt(i) & 0x3FF;

        lastPos = i + 1;
        c = 0x10000 + (((c & 0x3FF) << 10) | c2);
        out += hexTable[0xF0 | (c >> 18)] +
        hexTable[0x80 | ((c >> 12) & 0x3F)] +
        hexTable[0x80 | ((c >> 6) & 0x3F)] +
        hexTable[0x80 | (c & 0x3F)];
    }
    if (lastPos === 0)
    return str;
    if (lastPos < len)
    return out + str.slice(str, lastPos);
    return out;
}

function decodeStr(s, decoder) {
    try {
        return decoder(s);
    } catch(e) {
        return qsUnescape(s, true);
    }
}

function charCodes(str) {
    if (str.length === 0) return [];
    if (str.length === 1) return [str.charCodeAt(0)];
    const ret = new Array(str.length);
    for (let i = 0; i < str.length; ++i)
    ret[i] = str.charCodeAt(i);
    return ret;
}

function encodeStringified(v, encode) {
    if (typeof v === 'string')
    return (v.length ? encode(v) : '');
    if (typeof v === 'number' && Number.isFinite(v)) {
        return (Math.abs(v) < 1e21 ? '' + v : encode('' + v));
    }
    if (typeof v === 'bigint')
    return '' + v;
    if (typeof v === 'boolean')
    return v ? 'true' : 'false';
    return '';
}

function encodeStringifiedCustom(v, encode) {
    return encode(stringifyPrimitive(v));
}

function stringifyPrimitive(v) {
    if (typeof v === 'string')
    return v;
    if (typeof v === 'number' && Number.isFinite(v))
    return '' + v;
    if (typeof v === 'bigint')
    return '' + v;
    if (typeof v === 'boolean')
    return v ? 'true' : 'false';
    return '';
}

//export const querystring = {
//    unescapeBuffer,
//    parse,
//    stringify,
//    escape: qsEscape,
//    unescape: qsUnescape,
//    encode: stringify,
//    decode: parse,
//    encodeStr,
//    hexTable
//}

module.exports = {
    unescapeBuffer,
    parse,
    stringify,
    escape: qsEscape,
    unescape: qsUnescape,
    encode: stringify,
    decode: parse,
    encodeStr,
    hexTable
}